package com.wiringweb.servlet;

import com.gooddies.reflection.ReflectionUtils;
import com.gooddies.utils.IterableEnumeration;
import com.gooddies.utils.Pair;
import com.gooddies.wiring.Wiring;
import com.gooddies.wiring.WiringBuilder;
import com.gooddies.wiring.WiringComponentHolder;
import com.wiringweb.annotations.Controller;
import com.wiringweb.annotations.PostRequest;
import com.wiringweb.annotations.PreRequest;
import com.wiringweb.annotations.UrlBinding;
import com.wiringweb.data.FormData;
import com.wiringweb.data.QueryParameters;
import com.wiringweb.exceptions.ExceptionHandler;
import com.wiringweb.exceptions.WiringWebException;
import com.wiringweb.http.HttpMethod;
import com.wiringweb.parameterprocessor.ParameterProcessorSettings;
import com.wiringweb.parameterprocessor.annotation.PathVariable;
import com.wiringweb.parameterprocessor.annotation.QueryParameter;
import com.wiringweb.parameterprocessor.implementation.FormDataImpl;
import com.wiringweb.parameterprocessor.implementation.HttpRequestImpl;
import com.wiringweb.parameterprocessor.implementation.HttpResponseImpl;
import com.wiringweb.parameterprocessor.implementation.PathVariableImpl;
import com.wiringweb.parameterprocessor.implementation.QueryParameterImpl;
import com.wiringweb.parameterprocessor.implementation.QueryParametersImpl;
import com.wiringweb.resolution.Resolution;
import com.wiringweb.resolution.VelocityResolution;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * @author Dmitry
 */
public class WiringServlet extends HttpServlet {

    private final Map<HttpMethod, UrlsContainer> methodsToUrlsMap;
    private final Pattern controllerPrefixCheckPattern = Pattern.compile("/?([\\w\\d]+)/?",
            Pattern.UNICODE_CHARACTER_CLASS);
    private WiringRequestProcessor requestProcessor;
    private ParameterProcessorSettings parameterProcessorSettings;
    private Map<BaseWiringWebController, Pair<Method, Method>> controllerToPrePostMethods = new HashMap<BaseWiringWebController, Pair<Method, Method>>();
    private Map<Method, Pair<Method, Method>> methodToPrePostMethods = new HashMap<Method, Pair<Method, Method>>();
    private ExceptionHandler exceptionHandler = new ExceptionHandler();

    public WiringServlet() {
        methodsToUrlsMap = new EnumMap<HttpMethod, UrlsContainer>(HttpMethod.class);
        initParameterProcessorSettings();
    }

    private void initParameterProcessorSettings() {
        parameterProcessorSettings = new ParameterProcessorSettings();
        parameterProcessorSettings.addAnnotation(PathVariable.class, new PathVariableImpl());
        parameterProcessorSettings.addAnnotation(HttpServletRequest.class, new HttpRequestImpl());
        parameterProcessorSettings.addAnnotation(HttpServletResponse.class, new HttpResponseImpl());
        parameterProcessorSettings.addAnnotation(QueryParameters.class, new QueryParametersImpl());
        parameterProcessorSettings.addAnnotation(QueryParameter.class, new QueryParameterImpl());
        parameterProcessorSettings.addAnnotation(FormData.class, new FormDataImpl());
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        System.out.println("Init wiring servlet");
        WiringBuilder builder = Wiring.init();

        for (String paramName : IterableEnumeration.make(config.getInitParameterNames())) {
            boolean processed = false;
            String paramValue = config.getInitParameter(paramName);
            if (paramName.equals("package-scan")) {
                System.out.println("package-scan = " + paramValue);
                builder.addScanPackage(paramValue);
                processed = true;
            }
            if (paramName.equals("velocityBasePath")) {
                VelocityResolution.setBasePath(paramValue);
                processed = true;
            }
            if (!processed) {
                throw new ServletException("Unknown parameter [" + paramName + "] in WiringServlet initialization in web.xml can be only ['package-scan','velocityBasePath']");
            }
        }

        builder.build();
        List<BaseWiringWebController> controllers = processControllers();
        requestProcessor = new WiringRequestProcessor(methodsToUrlsMap);
        for (BaseWiringWebController controller : controllers) {
            controller.setWiringRequestProcessor(requestProcessor);
        }

        requestProcessor.setControllerToPrePostMethods(controllerToPrePostMethods);
        requestProcessor.setMethodToPrePostMethods(methodToPrePostMethods);
        requestProcessor.setExceptionHandler(exceptionHandler);
        VelocityResolution.init();
    }

    private List<BaseWiringWebController> processControllers() {
        List<BaseWiringWebController> controllersList = new ArrayList<BaseWiringWebController>(50);
        for (WiringComponentHolder holder : Wiring.get().getAllWiringComponents()) {
            if (holder.getClazz().isAnnotationPresent(Controller.class)) {
                if (!holder.isSingleton()) {
                    throw new WiringWebException("Controller " + holder.getClazz().getName() + " should be singleton(WiringComponent(singleton=true))");
                }

                if (!ReflectionUtils.isSubclass(BaseWiringWebController.class, holder.getClazz())) {
                    throw new WiringWebException("Controller " + holder.getClazz().getName() + " should be extended from BaseWiringWebController class");
                }

                Controller annotation = (Controller) holder.getClazz().getAnnotation(Controller.class);
                String prefix = annotation.value();
                BaseWiringWebController controller = (BaseWiringWebController) holder.getObject();
                prefix = checkControllerPrefix(controller, prefix);
                processController(controller, prefix);
                controllersList.add(controller);
            }
        }

        return controllersList;
    }

    private String checkControllerPrefix(BaseWiringWebController controller, String prefix) {
        if (prefix != null && !prefix.isEmpty()) {
            Matcher matcher = controllerPrefixCheckPattern.matcher(prefix);
            if (!matcher.matches()) {
                throw new WiringWebException(String.format("Controller %s prefix should be empty string (\"\") or one word string with or without slashes (/prefix/, /prefix, prefix/, prefix)", controller.getClass().getName()));
            }

            return matcher.group(1);
        }

        return prefix;
    }

    private void processController(final BaseWiringWebController controller, final String controllerPrefix) {
        System.out.println("WiringWebController found:" + controller.getClass().getName());
        final List<Method> methodsOfController = new ArrayList<Method>();
        final Pair<Method, Method> controllerPrePostHandlers = new Pair<>();
        ReflectionUtils.iterateMethods(controller.getClass(), new ReflectionUtils.MethodResult() {
            @Override
            public void method(Method m) {
                if (m.isAnnotationPresent(UrlBinding.class)) {
                    UrlBinding bindAnnotation = m.getAnnotation(UrlBinding.class);
                    if (!ReflectionUtils.isSubclass(Resolution.class, m.getReturnType())) {
                        throw new WiringWebException("Method " + m.getName() + " of controller " + controller.getClass().getName() + " should return Resolution object");
                    }

                    processMethod(m, bindAnnotation, controller, controllerPrefix);
                    methodsOfController.add(m);
                }
                if (m.isAnnotationPresent(com.wiringweb.annotations.ExceptionHandler.class)) {
                    processExceptionHandler(controller, m, m.getAnnotation(com.wiringweb.annotations.ExceptionHandler.class));
                }
            }
        });

        ReflectionUtils.iterateMethods(controller.getClass(), new ReflectionUtils.MethodResult() {
            @Override
            public void method(Method m) {
                if (m.isAnnotationPresent(PreRequest.class)) {
                    if (m.getParameterTypes().length != 1 || !(m.getParameterTypes()[0] == HttpServletRequest.class || m.getParameterTypes()[0] == HttpServletResponse.class)) {
                        String message = String.format("Prerequest method [%s] in class [%s] should accept only one argument of type HttpServletRequest or HttpServletResponse", m.getName(), controller.getClass().getName());
                        throw new WiringWebException(message);
                    }
                    if (!m.isAccessible()) {
                        m.setAccessible(true);
                    }
                    PreRequest preRequest = m.getAnnotation(PreRequest.class);
                    if (preRequest.method().isEmpty()) {
                        if (controllerPrePostHandlers.getFirst() != null) {
                            String message = String.format("controller [%s] already has prerequest method [%s] but you try to provide one more [%s]", controller.getClass().getName(), controllerPrePostHandlers.getFirst().getName(), m.getName());
                            throw new WiringWebException(message);
                        }

                        controllerPrePostHandlers.setFirst(m);
                    } else {
                        Method controllerMethod = findControllerMethodByName(preRequest.method(), methodsOfController);
                        if (controllerMethod == null) {
                            String message = String.format("You try to add prerequest method [%s] to inexist method [%s] in controller %s", m.getName(), preRequest.method(), controller.getClass().getName());
                            throw new WiringWebException(message);
                        }
                        Pair<Method, Method> methods = methodToPrePostMethods.get(controllerMethod);
                        if (methods == null) {
                            methods = new Pair<>(m, null);
                            methodToPrePostMethods.put(controllerMethod, methods);
                        } else {
                            if (methods.getFirst() != null) {
                                String message = String.format("controller [%s] already has prerequest method [%s] for method [%s] but you try to provide one more [%s]", controller.getClass().getName(), methods.getFirst(), controllerMethod.getName(), m.getName());
                                throw new WiringWebException(message);
                            } else {
                                methods.setFirst(m);
                            }
                        }
                    }
                }
                if (m.isAnnotationPresent(PostRequest.class)) {
                    if (m.getParameterTypes().length != 2 || m.getParameterTypes()[0] != HttpServletRequest.class) {
                        String message = String.format("Postrequest method [%s] in class [%s] should accept only two argument of type HttpServletRequest and Object", m.getName(), controller.getClass().getName());
                        throw new WiringWebException(message);
                    }
                    if (!m.isAccessible()) {
                        m.setAccessible(true);
                    }
                    PostRequest postrequest = m.getAnnotation(PostRequest.class);
                    if (postrequest.method().isEmpty()) {
                        if (controllerPrePostHandlers.getSecond() != null) {
                            String message = String.format("controller [%s] already has postrequest method [%s] but you try to provide one more [%s]", controller.getClass().getName(), controllerPrePostHandlers.getFirst().getName(), m.getName());
                            throw new WiringWebException(message);
                        }

                        controllerPrePostHandlers.setSecond(m);
                    } else {
                        Method controllerMethod = findControllerMethodByName(postrequest.method(), methodsOfController);
                        if (controllerMethod == null) {
                            String message = String.format("You try to add postrequest method [%s] to inexist method [%s] in controller %s", m.getName(), postrequest.method(), controller.getClass().getName());
                            throw new WiringWebException(message);
                        }
                        Pair<Method, Method> methods = methodToPrePostMethods.get(controllerMethod);
                        if (methods == null) {
                            methods = new Pair<>(null, m);
                            methodToPrePostMethods.put(controllerMethod, methods);
                        } else {
                            if (methods.getSecond() != null) {
                                String message = String.format("controller [%s] already has postrequest method [%s] for method [%s] but you try to provide one more [%s]", controller.getClass().getName(), methods.getFirst(), controllerMethod.getName(), m.getName());
                                throw new WiringWebException(message);
                            } else {
                                methods.setSecond(m);
                            }
                        }
                    }
                }
            }
        });
        if (controllerPrePostHandlers.getFirst() != null || controllerPrePostHandlers.getSecond() != null) {
            controllerToPrePostMethods.put(controller, controllerPrePostHandlers);
        }
    }

    private void processExceptionHandler(BaseWiringWebController controller, Method method, com.wiringweb.annotations.ExceptionHandler annotation) {
        method.setAccessible(true);
        exceptionHandler.addExceptionHandler(controller, method, annotation.value());
    }

    private Method findControllerMethodByName(String name, List<Method> methods) {
        for (Method m : methods) {
            if (m.getName().equals(name)) {
                return m;
            }
        }
        return null;
    }

    private void processMethod(Method method, UrlBinding bindAnnotation, BaseWiringWebController controller, String controllerPrefix) {
        UrlsContainer container = methodsToUrlsMap.get(bindAnnotation.httpMethod());
        if (container == null) {
            container = new UrlsContainer();
            methodsToUrlsMap.put(bindAnnotation.httpMethod(), container);
        }

        Pattern pattern = Pattern.compile(bindAnnotation.value());
        System.out.println("  WiringWebController url:method" + method.getName() + ":" + bindAnnotation.value());
        container.addUrl(parameterProcessorSettings, controllerPrefix, pattern, controller, method, bindAnnotation.httpMethod());
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            requestProcessor.processRequest(req, resp, getServletConfig());
        } catch (Exception ex) {
            throw new WiringWebException("Wiring exception "+ex, ex);
        }
    }
}
